<?php

/**
 *******************************************************************************
 * 
 * CLASS [ dbo ]
 * 
 *******************************************************************************
 * 
 * Classe generique de manipulation de donnees permettant de representer
 * n'importe quel objet
 * 
 *******************************************************************************
 * 
 * @package objects
 * @version 20070807
 * @author Stanislas PERRIN <stanislas.perrin@gmail.com> <stan@extramuros.com>
 * @author Johann PAVIS <johann.pavis@gmail.com>
 * 
 * @licence GNU/LGPL v3
 * 
 * 
 * Attention !
 * Cette classe a besoin de la classe debug.class.php qui se trouve ici :
 * http://svn.extramuros.com/libs/debug.class.php
 *  
 */

abstract CLASS dbo EXTENDS db
{
	#########################################################################
	#
	# PROPRIETES
	#
	#########################################################################
	
	// table ou est stockee l'objet
	protected $table = '';

	// nom du champ « id » de la table
	protected $idFieldName = 'id';
	
	// id de l'objet ( = id de l'enregistrement dans la table)
	protected $id = NULL;
	
	// Structure du tableau des proprietes de l'objet : $properties[champ] = valeur
	protected $properties = array();

	// nombre de proprietes
	protected $nbProp = 0;
	
	// est un nouvel objet
	protected $isNew = TRUE;
	
	// singletonisation : compteur d'instances sur les ID
	protected static $singleton = array();
	
	// Etat de l'objet
	protected $state = 0;
	protected $stateHistory = array();
	
	// Lock (pour éviter les écritures simultanées)
	protected static $lock = array();
	
	// Flag "modifié" : indique un déséquilibre entre l'objet et sa représentation
	// dans la bdd. Un objet dont les propriétés ont été modifiés mais pas encore
	// sauvegardées (méthode save() pas encore appellée) a son statut modifié à VRAI.
	// Une fois la méthode save() appellée, le statut "modifié" passe à FAUX.
	protected static $modified = array();
	
	#########################################################################
	#
	# CONSTRUCTEUR
	#
	#########################################################################
	
	public function __construct($dbConnectionObject=FALSE,$table=FALSE)
	{
		if ($dbConnectionObject && $table)
		{
			$this->table = $table;
			parent::__construct($dbConnectionObject);
		}
		else
		{
			throw new debug('erreur dans le constructeur de ' . get_class($this),-100);
		}
	}
	

	#########################################################################
	#
	# DESTRUCTEUR
	#
	#########################################################################
	
	public function __destruct()
	{
		// Suppression de l'instance dans le compteur "singleton"
		// if (isset(self::$singleton[$this->id])) self::$singleton[$this->id]--;
		$this->reset();
		$this->close();
	}
	
	#########################################################################
	#
	# METHODES PUBLIQUES
	#
	#########################################################################
	
	/**
	* open()
	*
	* "ouvre" un objet en chargeant ses proprietes depuis la BDD
	*/
	public function open($id=NULL)
	{
		// declarations
		$result = FALSE;
		
		$this->id = $id;
		//$this->table = $table;

		if (!is_null($this->id) && !empty($this->id))
		{
			/*
			// Test singleton	
			self::$singleton[$this->id]++;	// incrementation du compteur d'instance(s) pour l'ID
			$nombreInstancesPourID = self::$singleton[$this->id];
			// Si le nombre d'instances pour un ID est supérieur à 1
			if($nombreInstancesPourID > 1) 
			{
				// decrementation du compteur d'instances avant reset de l'instance...
				self::$singleton[$this->id]--;
				// reset
				$this->reset();
				// levage d'exception !
				throw new debug("nombre d'instance ($nombreInstancesPourID) pour l'objet ayant l'ID {$this->id} superieur a 1",-321);
			}
			*/
			$this->queryResultRC = $this->query("SELECT * FROM `{$this->table}` WHERE `{$this->table}`.`{$this->idFieldName}` = '{$this->id}'");
		}
		else
		{
			$this->properties = array();
			$this->nbProp = count($this->properties);
			return $this->isNew;
		}
			
		
		//if ($result && $this->fields > 0)
		if ($this->queryResultRC)
		{
			// lit les champs dans le resultat et remplis la table des proprietes
			while ($line = mysql_fetch_assoc($this->queryResultRC))
			{
				foreach ($line as $propertyName => $propertyValue)
				{
					$this->properties[$propertyName] = $this->convertStringCharset($propertyValue);
				}
			}
			
			// Comptage du nombre de proprietes
			$this->nbProp = count($this->properties);
			
			// Si il y a des résultats, c'est que ce n'est pas un nouvel enregistrement
			$this->isNew = FALSE;
			
			return TRUE;
		}
		else
		{
			// S'il n'y a pas de résultats, c'est que c'est un nouvel enregistrement
			$this->isNew = TRUE;
		}
		
		return FALSE;
	}
	
	/**
	* save()
	*
	* sauvegarde l'objet
	* /!\ plante en milieu de sauvegarde et retourne FALSE si saveValue() retourne 1 seul FALSE
	* (du coup on peut avoir des objets a moitie enregistre si y'a une valeur qui merdoie)
	*
	* @param
	* @return boolean VRAI si réussi, FAUX sinon
	*/	
	public function save()
	{
		// Avant toutes choses : vérification du flag "modifié"
		/*
		if (self::$modified[get_class($this)][$this->id])
		{
			$classe = get_class($this);
			throw new debug(get_class($this) . " : l'objet de classe $classe (id={$this->id}) a ete modifie",-998);
		}
		*/
		
		// Gestion du lock : on ne teste l'existence d'un lock qui si l'objet n'est pas
		// nouveau. Si l'objet est nouveau, il ne *peut pas* y avoir un accès concurrent !
		/*
		if (!$this->isNew)
		{
			if (self::$lock[get_class($this)][$this->id] > 0)
			{
					throw new debug(get_class($this) . ' : acces concurrent detecte',-999);
			}
			elseif (!isset(self::$lock[get_class($this)][$this->id]) || self::$lock[get_class($this)][$this->id] == 0)
			{
				self::$lock[get_class($this)][$this->id]++;
				
				if (self::$lock[get_class($this)][$this->id] > 1)
				{
					self::$lock[get_class($this)][$this->id]--;
					throw new debug(get_class($this) . ' : acces concurrent detecte',-999);
				}
			}
		}
		*/
		
		$returnValue = FALSE;
		
		foreach($this->properties as $champ => $valeur)
		{
			try
			{
				$this->saveValue($champ,$valeur);
			}
			catch(debug $d)
			{
				// return FALSE;
			}
			
		}
		
		// Flag "modifié" => FAUX (l'objet est conforme à sa représentation
		// dans la bdd)
		self::$modified[get_class($this)][$this->id] = FALSE;
		
		// Retirer le lock
		self::$lock[get_class($this)][$this->id] = 0;
		
		return TRUE;
	}
	
	/**
	* update()
	*
	* met a jour l'objet en rappellant la methode open()
	*/
	public function update($id=FALSE)
	{
		if ($id) $this->setId($id);
		return $this->open($this->id);
	}
	
	/**
	* reload()
	*
	* alias pour update()
	*/
	public function reload($id=FALSE)
	{
		return $this->update($id);
	}
	
	/**
	* reset()
	*/
	public function reset()
	{
		$this->id = 0;
		$this->properties = array();
		$this->isNew = TRUE;
	}
	
	/**
	* delete()
	*
	*/
	public function delete()
	{
		if (!$this->query("DELETE FROM `{$this->table}` WHERE `{$this->idFieldName}` = '{$this->id}'")) throw new debug("impossible de supprimer l'objet ",-152);
		if (!$this->close()) throw new debug("impossible de fermer l'objet",-153);
	}
	
	/**
	* close()
	*
	* ferme l'objet (vide son ID et ses proprietes)
	*/
	public function close()
	{
		unset ($this);
	}
	
	/**
	 * search()
	 * 
	 * recherche des objets 
	 * 
	 * @param string $champ nom du champ dans lequel on recherche
	 * @param string $pattern sequence recherchee (au format SQL)
	 * @return array tableau de resultat
	 */	
	 public function search($field=FALSE,$pattern=FALSE)
	 {
	 	// Inits
	 	$resultat = array();
	 	
		if (!$field || !$pattern) throw new debug(get_class($this) . ' : arguments incorrects',-100);
		
		// Requete
		$query = "SELECT * FROM `{$this->table}` WHERE `{$this->table}`.`$field` LIKE '$pattern' ORDER BY `{$this->table}`.`$field` ASC;";
		// Execution
		$resultatRequete = $this->query($query);
		// Parcours des resultats
		while($ligne = mysql_fetch_assoc($resultatRequete))
		{
			array_push($resultat,$ligne);
		}
		// Retour
		return $resultat;
	 }

	/**
	 * getTuplesByCondition()
	 */
	 public function getTuplesByCondition($query=FALSE,$condition=FALSE)
	 {
 		if (!$query || !$condition) throw new debug(get_class($this) . " : requete ($query) ou condition ($condition) vide(s) ou invalide(s)",-777);
 		return $this->getTuples("$query WHERE $condition");
	 }
	 
	 /**
	  * getIsNew()
	  */
	 public function getIsNew()
	 {
		 return $this->isNew;
	 }


	#########################################################################
	#
	# GESTION DES ETATS DE L'OBJET
	#
	#########################################################################
	
	/**
	 * 
	 * getLastState()
	 * 
	 */
	 public function getLastState()
	 {
	 	
	 } 

	/**
	 * 
	 * setState()
	 * 
	 */
	 public function setState()
	 {
	 	
	 } 
	 
	#########################################################################
	#
	# GETTERS & SETTERS
	#
	#########################################################################
	
	/**
	* set()
	*
	* modifie la valeur d'une propriete de l'objet
	*
	* @param string $name nom de la propriete dont on modifie la valeur
	* @param mixed $value valeur a mettre dans $name
	*/
	public function set($name=FALSE,$value=null)
	{
		// Pas de test sur $value qui peut etre vide
		if (!$name) throw new debug (get_class($this) . " : nom de valeur ($name) vide ou invalide !",-100);
		// On indique que l'objet a été modifié
		self::$modified[get_class($this)][$this->id] = TRUE;
		//
		return $this->properties[$name] = $value;
	}

	/**
	* setProperty()
	*
	* alias de set()
	*
	* @param string $name nom de la propriete dont on modifie la valeur
	* @param mixed $value valeur a mettre dans $name
	*/
	public function setProperty($name=NULL,$value=NULL)
	{
		return $this->set($name,$value);
	}	
	
	/*
	* setAllProperties()
	*
	* permet de remplir le tableau des proprietes en une seule operation
	* (utile pour echanger des donnees avec un autre objet)
	*
	* @param array() $p tableau de proprietes de l'objet
	*/
	public function setAllProperties($p=array())
	{
		if (!empty($p))
		{
			$this->properties = $p;
		}
		else
		{
			return FALSE;
		}
	}
	
	/**
	* getIdByCondition()
	*
	* permet d'ouvrir un objet en recherchant ses proprietes depuis le resultat
	* d'une requete SQL sur des conditions 'WHERE'
	*/
	public function getIdByCondition($conditions)
	{
		$this->query("SELECT `{$this->table}`.`{$this->idFieldName}` FROM `{$this->table}` WHERE $conditions");
		$tmp = mysql_fetch_assoc($this->queryResultRC);
		$this->setId($tmp["{$this->idFieldName}"]*1);
		$this->update();
		return $this->id;
	}
	
	
	/**
	* getId()
	*
	* renvoie l'ID de l'objet
	*
	* @param
	*/
	public function getId()
	{
		return $this->id;
	}
	
	
	/**
	* getTable()
	*
	* retourne le nom de la table manipulee au travers de l'objet
	*/
	public function getTable()
	{
		return $this->table;
	}

	/**
	* get()
	*
	* retourne la valeur d'une propriete
	*
	* @param string name nom de la propriete dont on souhaite la valeur
	*/
	public function get($name)
	{
		return $this->properties[$name];
	}

	/**
	* getProperty()
	*
	* alias de get()
	*
	* @param string name nom de la propriete dont on souhaite la valeur
	*/
	public function getProperty($name=FALSE)
	{
		return ($name) ? $this->get($name) : FALSE;
	}
	
	/**
	* getAllProperties()
	*
	* permet de recuperer le tableau des proprietes
	* (utile pour echanger des donnees avec un autre objet)
	* 
	* @return array liste des proprietes de l'objet et de leurs valeurs sous forme de tableau associatif $propriete => $valeur
	*/
	public function getAllProperties()
	{
		return $this->properties;
	}	

	#########################################################################
	#
	# METHODES MAGIQUES
	# 
	#########################################################################

	/**
	 * __toString()
	 */
	public function __toString()
	{
		return implode('|',$this->properties);
	}

	/**
	* _this();
	*
	* methode magique qui retourne l'objet lui-meme (utile pour constituer une collection d'objets)
	*/
	public function _this()
	{
		return $this;
	}
	
	/**
	 * _id()
	 * 
	 */
	 /*
	 public function _id()
	 {
	 	return $this->id;
	 }*/

	/**
	 * _table()
	 * 
	 */
	 public function _id()
	 {
	 	return $this->table;
	 }
	
	
	
	#########################################################################
	#
	# GETTERS & SETTERS SPECIFIQUES
	#
	#########################################################################
	
	/**
	 * 
	 */
	 public function getSingletonArray()
	 {
	 	return self::$singleton;
	 }
	
	
	#########################################################################
	#
	# METHODE PRIVEES
	#
	#########################################################################
		
	/*
	* saveValue()
	*
	* enregistre une propriete
	*
	* @param string $field nom du champ dont la valeur va etre ajoutee
	* @param string $value valeur du champ $field
	*/
	private function saveValue($field=FALSE,$value=NULL)
	{
		// declarations
		$result = FALSE;
		
		// pas de test de $value, car elle a le droit d'etre NULL
		if (!$field) throw new debug('saveValue : valeurs de parametres incorrects',-100);
		
		// Conversion de $value dans le "bon" charset + échappements
		$value = addslashes($this->convertStringCharset($value));
		
		// ID == 0 -> C'est un nouvel objet, on fait des INSERT
		if ($this->isNew)
		{
			$result = $this->query("INSERT INTO `{$this->table}` SET `$field` = '$value';");
			
			// Comme c'est un nouvel enregistrement, on ne dispose pas de son ID
			// on va donc le chercher
			$newId = mysql_insert_id($this->dbConnectionId);
			if ($newId == 0) throw new debug('saveValue : insertID = 0 !',-100);
			else $this->setId(mysql_insert_id($this->dbConnectionId));
		}
		// ID > 0 -> C'est un objet existant, on fait juste un UPDATE
		else
		{
			$result = $this->query("UPDATE `{$this->table}` SET `$field` = '$value' WHERE `{$this->table}`.`{$this->idFieldName}` = '{$this->id}';");
		}
		
		return $result;
	}

	/*
	* saveValues()
	*
	* enregistre une propriete
	*
	* @param string $field nom du champ dont la valeur va etre ajoutee
	* @param string $value valeur du champ $field
	*/
	private function saveValues($values=FALSE)
	{
		// declarations
		$result = FALSE;
		
		// pas de test de $value, car elle a le droit d'etre NULL
		if (count($values) == 0) throw new debug('saveValues : valeurs de parametres incorrects',-100);
		
		// inits
		$sets = '';
		
		// Conversion de $value dans le "bon" charset + échappements
		foreach($values as $field => $value)
		{
			$values[$field] = addslashes($this->convertStringCharset($value));
			$sets .= "`{$this->table}`.`$field` = '$value', ";
		}
		
		$sets = substr($sets,0,str_len($sets)-2);

		
		// ID == 0 -> C'est un nouvel objet, on fait des INSERT
		if ($this->isNew)
		{
			$result = $this->query("INSERT INTO `{$this->table}` SET $sets;");
			
			// Comme c'est un nouvel enregistrement, on ne dispose pas de son ID
			// on va donc le chercher
			$this->setId(mysql_insert_id($this->dbConnectionId));
		}
		// ID > 0 -> C'est un objet existant, on fait juste un UPDATE
		else
		{
			$result = $this->query("UPDATE `{$this->table}` SET $sets WHERE `{$this->table}`.`{$this->idFieldName}` = '{$this->id}';");
		}
		
		return $result;
	}
	
	/*
	* setId()
	*
	* met a jour l'ID de l'enregistrement de l'objet
	*
	* @param int $id identifiant a mettre a jour
	* @return 
	*/
	private function setId($id)
	{
		$this->id = $id;
		
		if ($this->id > 0)
		{
			$this->isNew = FALSE;
			return $this->isNew;
		}
		else
		{
			$this->isNew = TRUE;
			return $this->isNew;
		}
		
	}

	/**
	 * 
	 * setIdFieldName()
	 * 
	 * règle / met à jour le nom du champs qui représente la clef unique
	 * des tuples de la table
	 * 
	 * @param string $fieldName nom du champ de la table qui représente la clef unique
	 * @return boolean toujours vrai
	 */
	protected function setIdFieldName($fieldName=FALSE)
	{
		$this->idFieldName = ($fieldName) ? $fieldName : 'id';
		return TRUE;
	}



}



?>
